/*
 * File    : TRTrackerServerProcessorUDP.java
 * Created : 20-Jan-2004
 * By      : parg
 * 
 * Azureus - a Java Bittorrent client
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details ( see the LICENSE file ).
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.gudy.azureus2.core3.tracker.server.impl.udp;

/**
 * @author parg
 *
 */

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.tracker.protocol.PRHelpers;
import org.gudy.azureus2.core3.tracker.protocol.udp.PRUDPPacketReplyAnnounce;
import org.gudy.azureus2.core3.tracker.protocol.udp.PRUDPPacketReplyAnnounce2;
import org.gudy.azureus2.core3.tracker.protocol.udp.PRUDPPacketReplyConnect;
import org.gudy.azureus2.core3.tracker.protocol.udp.PRUDPPacketReplyError;
import org.gudy.azureus2.core3.tracker.protocol.udp.PRUDPPacketReplyScrape;
import org.gudy.azureus2.core3.tracker.protocol.udp.PRUDPPacketReplyScrape2;
import org.gudy.azureus2.core3.tracker.protocol.udp.PRUDPPacketRequestAnnounce;
import org.gudy.azureus2.core3.tracker.protocol.udp.PRUDPPacketRequestAnnounce2;
import org.gudy.azureus2.core3.tracker.protocol.udp.PRUDPPacketRequestScrape;
import org.gudy.azureus2.core3.tracker.protocol.udp.PRUDPPacketTracker;
import org.gudy.azureus2.core3.tracker.protocol.udp.PRUDPTrackerCodecs;
import org.gudy.azureus2.core3.tracker.server.TRTrackerServerPeer;
import org.gudy.azureus2.core3.tracker.server.TRTrackerServerRequest;
import org.gudy.azureus2.core3.tracker.server.impl.TRTrackerServerPeerImpl;
import org.gudy.azureus2.core3.tracker.server.impl.TRTrackerServerProcessor;
import org.gudy.azureus2.core3.tracker.server.impl.TRTrackerServerTorrentImpl;
import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.HashWrapper;
import org.gudy.azureus2.core3.util.RandomUtils;
import org.gudy.azureus2.core3.util.SHA1Hasher;
import org.gudy.azureus2.core3.util.SystemTime;

import com.aelitis.net.udp.uc.PRUDPPacket;
import com.aelitis.net.udp.uc.PRUDPPacketRequest;

public class TRTrackerServerProcessorUDP extends TRTrackerServerProcessor {
    private static final LogIDs LOGID = LogIDs.TRACKER;
    // client may connect + then retry announce up to 4 times -> * 6

    public static final long CONNECTION_ID_LIFETIME = PRUDPPacket.DEFAULT_UDP_TIMEOUT * 6;

    private TRTrackerServerUDP server;
    private DatagramSocket socket;
    private DatagramPacket request_dg;

    private static Map<Long, connectionData> connection_id_map = new LinkedHashMap<Long, connectionData>();
    private static Map<String, List<connectionData>> connection_ip_map = new HashMap<String, List<connectionData>>();
    private static long last_timeout_check;

    private static SecureRandom random = RandomUtils.SECURE_RANDOM;
    private static AEMonitor random_mon = new AEMonitor("TRTrackerServerUDP:rand");

    static {
        PRUDPTrackerCodecs.registerCodecs();
    }

    protected TRTrackerServerProcessorUDP(TRTrackerServerUDP _server, DatagramSocket _socket, DatagramPacket _packet) {
        server = _server;
        socket = _socket;
        request_dg = _packet;
    }

    public void runSupport() {
        byte[] input_buffer = new byte[request_dg.getLength()];

        System.arraycopy(request_dg.getData(), 0, input_buffer, 0, input_buffer.length);

        int packet_data_length = input_buffer.length;

        String auth_user = null;
        byte[] auth_user_bytes = null;
        byte[] auth_hash = null;

        if (server.isTrackerPasswordEnabled()) {

            // auth detail should be attached to the packet. Auth details are 16
            // bytes

            if (input_buffer.length < 17) {

                Logger.log(new LogEvent(LOGID, LogEvent.LT_WARNING, "TRTrackerServerProcessorUDP: " + "packet received but authorisation missing"));

                return;
            }

            packet_data_length -= 16;

            auth_user_bytes = new byte[8];

            auth_hash = new byte[8];

            System.arraycopy(input_buffer, packet_data_length, auth_user_bytes, 0, 8);

            int user_len = 0;

            while (user_len < 8 && auth_user_bytes[user_len] != 0) {

                user_len++;
            }

            auth_user = new String(auth_user_bytes, 0, user_len);

            System.arraycopy(input_buffer, packet_data_length + 8, auth_hash, 0, 8);
        }

        DataInputStream is = new DataInputStream(new ByteArrayInputStream(input_buffer, 0, packet_data_length));

        try {
            String client_ip_address = request_dg.getAddress().getHostAddress();

            PRUDPPacketRequest request = PRUDPPacketRequest.deserialiseRequest(null, is);

            Logger.log(new LogEvent(LOGID, "TRTrackerServerProcessorUDP: packet received: " + request.getString()));

            PRUDPPacket reply = null;
            TRTrackerServerTorrentImpl torrent = null;

            if (auth_user_bytes != null) {

                // user name is irrelevant as we only have one at the moment

                // <parg_home> so <new_packet> = <old_packet> + <user_padded_to_8_bytes> + <hash>
                // <parg_home> where <hash> = first 8 bytes of sha1(<old_packet> + <user_padded_to_8> + sha1(pass))
                // <XTF> Yes

                byte[] sha1_pw = null;

                if (server.hasExternalAuthorisation()) {

                    try {
                        URL resource = new URL("udp://" + server.getHost() + ":" + server.getPort() + "/");

                        sha1_pw = server.performExternalAuthorisation(resource, auth_user);

                    } catch (MalformedURLException e) {

                        Debug.printStackTrace(e);

                    }

                    if (sha1_pw == null) {

                        Logger.log(new LogEvent(LOGID, LogEvent.LT_ERROR, "TRTrackerServerProcessorUDP: auth fails for user '" + auth_user + "'"));

                        reply = new PRUDPPacketReplyError(request.getTransactionId(), "Access Denied");
                    }
                } else {

                    sha1_pw = server.getPassword();
                }

                // if we haven't already failed then check the PW

                if (reply == null) {

                    SHA1Hasher hasher = new SHA1Hasher();

                    hasher.update(input_buffer, 0, packet_data_length);
                    hasher.update(auth_user_bytes);
                    hasher.update(sha1_pw);

                    byte[] digest = hasher.getDigest();

                    for (int i = 0; i < auth_hash.length; i++) {

                        if (auth_hash[i] != digest[i]) {

                            Logger.log(new LogEvent(LOGID, LogEvent.LT_ERROR, "TRTrackerServerProcessorUDP: auth fails for user '" + auth_user
                                    + "'"));

                            reply = new PRUDPPacketReplyError(request.getTransactionId(), "Access Denied");

                            break;
                        }
                    }
                }
            }

            int request_type = TRTrackerServerRequest.RT_UNKNOWN;

            if (reply == null) {

                if (server.isEnabled()) {

                    try {
                        int type = request.getAction();

                        if (type == PRUDPPacketTracker.ACT_REQUEST_CONNECT) {

                            reply = handleConnect(client_ip_address, request);

                        } else if (type == PRUDPPacketTracker.ACT_REQUEST_ANNOUNCE) {

                            Object[] x = handleAnnounceAndScrape(client_ip_address, request, TRTrackerServerRequest.RT_ANNOUNCE);

                            if (x == null) {

                                throw (new Exception("Connection ID mismatch"));
                            }

                            reply = (PRUDPPacket) x[0];
                            torrent = (TRTrackerServerTorrentImpl) x[1];

                            request_type = TRTrackerServerRequest.RT_ANNOUNCE;

                        } else if (type == PRUDPPacketTracker.ACT_REQUEST_SCRAPE) {

                            Object[] x = handleAnnounceAndScrape(client_ip_address, request, TRTrackerServerRequest.RT_SCRAPE);

                            if (x == null) {

                                throw (new Exception("Connection ID mismatch"));
                            }

                            reply = (PRUDPPacket) x[0];
                            torrent = (TRTrackerServerTorrentImpl) x[1];

                            request_type = TRTrackerServerRequest.RT_SCRAPE;

                        } else {

                            reply = new PRUDPPacketReplyError(request.getTransactionId(), "unsupported action");
                        }
                    } catch (Throwable e) {

                        // e.printStackTrace();

                        String error = e.getMessage();

                        if (error == null) {

                            error = e.toString();
                        }

                        reply = new PRUDPPacketReplyError(request.getTransactionId(), error);
                    }
                } else {

                    System.out.println("UDP Tracker: replying 'disabled' to " + client_ip_address);

                    reply = new PRUDPPacketReplyError(request.getTransactionId(), "UDP Tracker disabled");
                }
            }

            if (reply != null) {

                InetAddress address = request_dg.getAddress();

                ByteArrayOutputStream baos = new ByteArrayOutputStream();

                DataOutputStream os = new DataOutputStream(baos);

                reply.serialise(os);

                byte[] output_buffer = baos.toByteArray();

                DatagramPacket reply_packet = new DatagramPacket(output_buffer, output_buffer.length, address, request_dg.getPort());

                socket.send(reply_packet);

                server.updateStats(request_type, torrent, input_buffer.length, output_buffer.length);
            }

        } catch (Throwable e) {

            Logger.log(new LogEvent(LOGID, "TRTrackerServerProcessorUDP: processing fails", e));
        } finally {

            try {
                is.close();

            } catch (Throwable e) {

            }
        }
    }

    public void interruptTask() {
    }

    protected long allocateConnectionId(String client_address) {
        try {
            random_mon.enter();

            long id = random.nextLong();

            Long new_key = new Long(id);

            connectionData new_data = new connectionData(client_address, id);

            // check for timeouts

            if (new_data.getTime() - last_timeout_check > 500) {

                last_timeout_check = new_data.getTime();

                Iterator<Long> it = connection_id_map.keySet().iterator();

                while (it.hasNext()) {

                    Long key = it.next();

                    connectionData data = connection_id_map.get(key);

                    if (new_data.getTime() - data.getTime() > CONNECTION_ID_LIFETIME) {

                        // System.out.println( "TRTrackerServerProcessorUDP: connection id timeout" );

                        it.remove();

                        List<connectionData> cds = connection_ip_map.get(client_address);

                        if (cds != null) {

                            Iterator<connectionData> it2 = cds.iterator();

                            while (it2.hasNext()) {

                                if (it2.next().getID() == key) {

                                    it2.remove();

                                    break;
                                }
                            }

                            if (cds.size() == 0) {

                                connection_ip_map.remove(client_address);
                            }
                        }

                    } else {
                        // insertion order into map is time based - LinkedHashMap returns keys in same order

                        break;
                    }
                }
            }

            List<connectionData> cds = connection_ip_map.get(client_address);

            if (cds == null) {

                cds = new ArrayList<connectionData>();

                connection_ip_map.put(client_address, cds);
            }

            cds.add(new_data);

            if (cds.size() > 512) {

                connectionData dead = cds.remove(0);

                connection_id_map.remove(dead.getID());
            }

            connection_id_map.put(new_key, new_data);

            // System.out.println( "TRTrackerServerProcessorUDP: allocated:" + id + ", connection id map size = " + connection_id_map.size());

            return (id);

        } finally {

            random_mon.exit();
        }
    }

    protected boolean checkConnectionId(String client_address, long id) {
        try {
            random_mon.enter();

            Long key = new Long(id);

            connectionData data = (connectionData) connection_id_map.get(key);

            if (data == null) {

                // System.out.println( "TRTrackerServerProcessorUDP: rejected:" + id + ", data not found" );

                return (false);

            } else {

                if (SystemTime.getMonotonousTime() - data.getTime() > CONNECTION_ID_LIFETIME) {

                    return (false);
                }
            }

            boolean ok = data.getAddress().equals(client_address);

            // System.out.println( "TRTrackerServerProcessorUDP: tested:" + id + "/" + client_address + " -> " + ok );

            return (ok);

        } finally {

            random_mon.exit();
        }
    }

    protected PRUDPPacket handleConnect(String client_ip_address, PRUDPPacketRequest request) {
        long conn_id = allocateConnectionId(client_ip_address);

        PRUDPPacket reply = new PRUDPPacketReplyConnect(request.getTransactionId(), conn_id);

        return (reply);
    }

    // returns reply packet and associated torrent if exists

    protected Object[] handleAnnounceAndScrape(String client_ip_address, PRUDPPacketRequest request, int request_type)

    throws Exception {
        if (!checkConnectionId(client_ip_address, request.getConnectionId())) {

            return (null);
        }

        List hashbytes = new ArrayList();
        HashWrapper peer_id = null;
        int port = 0;
        String event = null;

        long uploaded = 0;
        long downloaded = 0;
        long left = 0;
        int num_want = -1;

        String key = null;

        if (request_type == TRTrackerServerRequest.RT_ANNOUNCE) {

            if (PRUDPPacketTracker.VERSION == 1) {
                PRUDPPacketRequestAnnounce announce = (PRUDPPacketRequestAnnounce) request;

                hashbytes.add(announce.getHash());

                peer_id = new HashWrapper(announce.getPeerId());

                port = announce.getPort();

                int i_event = announce.getEvent();

                switch (i_event) {
                    case PRUDPPacketRequestAnnounce.EV_STARTED: {
                        event = "started";
                        break;
                    }
                    case PRUDPPacketRequestAnnounce.EV_STOPPED: {
                        event = "stopped";
                        break;
                    }
                    case PRUDPPacketRequestAnnounce.EV_COMPLETED: {
                        event = "completed";
                        break;
                    }
                }

                uploaded = announce.getUploaded();

                downloaded = announce.getDownloaded();

                left = announce.getLeft();

                num_want = announce.getNumWant();

                int i_ip = announce.getIPAddress();

                if (i_ip != 0) {

                    client_ip_address = PRHelpers.intToAddress(i_ip);
                }
            } else {

                PRUDPPacketRequestAnnounce2 announce = (PRUDPPacketRequestAnnounce2) request;

                hashbytes.add(announce.getHash());

                peer_id = new HashWrapper(announce.getPeerId());

                port = announce.getPort();

                int i_event = announce.getEvent();

                switch (i_event) {
                    case PRUDPPacketRequestAnnounce.EV_STARTED: {
                        event = "started";
                        break;
                    }
                    case PRUDPPacketRequestAnnounce.EV_STOPPED: {
                        event = "stopped";
                        break;
                    }
                    case PRUDPPacketRequestAnnounce.EV_COMPLETED: {
                        event = "completed";
                        break;
                    }
                }

                uploaded = announce.getUploaded();

                downloaded = announce.getDownloaded();

                left = announce.getLeft();

                num_want = announce.getNumWant();

                int i_ip = announce.getIPAddress();

                if (i_ip != 0) {

                    client_ip_address = PRHelpers.intToAddress(i_ip);
                }

                key = "" + announce.getKey();
            }
        } else {

            PRUDPPacketRequestScrape scrape = (PRUDPPacketRequestScrape) request;

            hashbytes.addAll(scrape.getHashes());
        }

        Map[] root_out = new Map[1];
        TRTrackerServerPeerImpl[] peer_out = new TRTrackerServerPeerImpl[1];

        TRTrackerServerTorrentImpl torrent =
                processTrackerRequest(server, "", root_out, peer_out, request_type, (byte[][]) hashbytes.toArray(new byte[0][0]), null, null,
                        peer_id, false, TRTrackerServerTorrentImpl.COMPACT_MODE_NONE,
                        key, // currently no "no_peer_id" / "compact" in the packet and anyway they aren't returned / key
                        event, false, port, 0, 0, client_ip_address, client_ip_address, downloaded, uploaded, left, num_want,
                        TRTrackerServerPeer.CRYPTO_NONE, (byte) 1, 0, null);

        Map root = root_out[0];

        if (request_type == TRTrackerServerRequest.RT_ANNOUNCE) {

            if (PRUDPPacketTracker.VERSION == 1) {
                PRUDPPacketReplyAnnounce reply = new PRUDPPacketReplyAnnounce(request.getTransactionId());

                reply.setInterval(((Long) root.get("interval")).intValue());

                List peers = (List) root.get("peers");

                int[] addresses = new int[peers.size()];
                short[] ports = new short[addresses.length];

                for (int i = 0; i < addresses.length; i++) {

                    Map peer = (Map) peers.get(i);

                    addresses[i] = PRHelpers.addressToInt(new String((byte[]) peer.get("ip")));

                    ports[i] = (short) ((Long) peer.get("port")).shortValue();
                }

                reply.setPeers(addresses, ports);

                return (new Object[] { reply, torrent });

            } else {

                PRUDPPacketReplyAnnounce2 reply = new PRUDPPacketReplyAnnounce2(request.getTransactionId());

                reply.setInterval(((Long) root.get("interval")).intValue());

                boolean local_scrape = client_ip_address.equals("127.0.0.1");

                Map scrape_details = torrent.exportScrapeToMap("", client_ip_address, !local_scrape);

                int seeders = ((Long) scrape_details.get("complete")).intValue();
                int leechers = ((Long) scrape_details.get("incomplete")).intValue();

                reply.setLeechersSeeders(leechers, seeders);

                List peers = (List) root.get("peers");

                int[] addresses = new int[peers.size()];
                short[] ports = new short[addresses.length];

                for (int i = 0; i < addresses.length; i++) {

                    Map peer = (Map) peers.get(i);

                    addresses[i] = PRHelpers.addressToInt(new String((byte[]) peer.get("ip")));

                    ports[i] = (short) ((Long) peer.get("port")).shortValue();
                }

                reply.setPeers(addresses, ports);

                return (new Object[] { reply, torrent });
            }

        } else {

            if (PRUDPPacketTracker.VERSION == 1) {

                PRUDPPacketReplyScrape reply = new PRUDPPacketReplyScrape(request.getTransactionId());

                /*
                 * Long interval = (Long)root.get("interval"); if ( interval != null ){ reply.setInterval(interval.intValue()); }
                 */

                Map files = (Map) root.get("files");

                byte[][] hashes = new byte[files.size()][];
                int[] s_complete = new int[hashes.length];
                int[] s_downloaded = new int[hashes.length];
                int[] s_incomplete = new int[hashes.length];

                Iterator it = files.keySet().iterator();

                int pos = 0;

                while (it.hasNext()) {

                    String hash_str = (String) it.next();

                    hashes[pos] = hash_str.getBytes(Constants.BYTE_ENCODING);

                    Map details = (Map) files.get(hash_str);

                    s_complete[pos] = ((Long) details.get("complete")).intValue();
                    s_incomplete[pos] = ((Long) details.get("incomplete")).intValue();
                    s_downloaded[pos] = ((Long) details.get("downloaded")).intValue();

                    pos++;
                }

                reply.setDetails(hashes, s_complete, s_downloaded, s_incomplete);

                return (new Object[] { reply, torrent });

            } else {

                PRUDPPacketReplyScrape2 reply = new PRUDPPacketReplyScrape2(request.getTransactionId());

                /*
                 * Long interval = (Long)root.get("interval"); if ( interval != null ){ reply.setInterval(interval.intValue()); }
                 */

                Map files = (Map) root.get("files");

                int[] s_complete = new int[files.size()];
                int[] s_downloaded = new int[s_complete.length];
                int[] s_incomplete = new int[s_complete.length];

                Iterator it = files.keySet().iterator();

                int pos = 0;

                while (it.hasNext()) {

                    String hash_str = (String) it.next();

                    Map details = (Map) files.get(hash_str);

                    s_complete[pos] = ((Long) details.get("complete")).intValue();
                    s_incomplete[pos] = ((Long) details.get("incomplete")).intValue();
                    s_downloaded[pos] = ((Long) details.get("downloaded")).intValue();

                    pos++;
                }

                reply.setDetails(s_complete, s_downloaded, s_incomplete);

                return (new Object[] { reply, torrent });
            }
        }
    }

    protected static class connectionData {
        private String address;
        private long id;
        private long time;

        private connectionData(String _address, long _id) {
            address = _address;
            id = _id;

            time = SystemTime.getMonotonousTime();
        }

        private String getAddress() {
            return (address);
        }

        private long getID() {
            return (id);
        }

        private long getTime() {
            return (time);
        }
    }
}
